#include "./openc.h"
#include "rgba32.h"

#include <assert.h>
#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>

#define OPENC_SWAP_VAR(type, v1, v2) do { type t = (v1); (v1) = (v2); (v2) = t; } while(0)
#define OPENC_CANVAS_POINT(c, x, y) (c)->pixels[(y) * (c)->stride + (x)]

static void openc_normalize_rect(int* x0, int* y0, int* w, int* h);
static void openc_sort_triangle_points_by_y(int* x1, int* y1,
                                           int* x2, int* y2,
                                           int* x3, int* y3);

static void openc_render_point_uncheck(openc_canvas_t* canvas, int x, int y, uint32_t color) {
    uint32_t origin_color = OPENC_CANVAS_POINT(canvas, x, y);
    OPENC_CANVAS_POINT(canvas, x, y) = rgba32_mix(origin_color, color);
}
                                        
openc_canvas_t openc_make_canvas(uint32_t* pixels, int width, int height) {
    openc_canvas_t c = {
        .pixels = pixels,
        .width = width,
        .height = height,
        .stride = width,
    };
    return c;
}


void openc_render_point(openc_canvas_t* canvas, int x, int y, uint32_t color) {
    if (0 <= x && x < canvas->width && 0 <= y && y < canvas->height) {
        openc_render_point_uncheck(canvas, x, y, color);
    }
}

void openc_draw_line(openc_canvas_t* canvas, int x1, int y1, int x2, int y2, uint32_t color) {
    int dx = x2 - x1;
    int dy = y2 - y1;

    if (dx != 0 && dy != 0) {
        double k = (double)dy/dx;

        if (fabs(k) < 1.f) {
            double c = y1 - k*x1;
            if (x1 > x2) OPENC_SWAP_VAR(int, x1, x2);
            for (int x = x1; x <= x2; ++x) {
                if (x < 0 || x >= canvas->width) {
                    continue;
                }
                int y = k * x + c;
                if (0 <= y && y < canvas->height) {
                    OPENC_CANVAS_POINT(canvas, x, y) = color;
                }
            }
        } else {
            k = (double)dx / dy;
            double c = x1 - k*y1;
            if (y1 > y2) OPENC_SWAP_VAR(int, y1, y2);
            for (int y = y1; y <= y2; ++y) {
                if (y < 0 || y >= canvas->height) {
                    continue;
                }
                int x = k * y + c;
                if (0 <= x && x < canvas->width) {
                    OPENC_CANVAS_POINT(canvas, x, y) = color;
                }
            }
        }
    } 
    else if (dx == 0) {
        if (x1 < 0 || x1 >= canvas->width) {
            return;
        }
        int x = x1;
        if (y1 > y2) OPENC_SWAP_VAR(int, y1, y2);
        for (int y = y1; y <= y2; ++y) {
            if (0 <= y && y < canvas->height) {
                OPENC_CANVAS_POINT(canvas, x, y) = color;
            }
        }
    } 
    else {
        if (y1 < 0 || y1 >= canvas->height) {
            return;
        }
        int y = y1;
        if (x1 > x2) OPENC_SWAP_VAR(int, x1, x2);
        for (int x = x1; x <= x2; ++x) {
            if (0 <= x && x < canvas->width) {
                OPENC_CANVAS_POINT(canvas, x, y) = color;
            }
        }
    }
}

void openc_draw_rect(openc_canvas_t* canvas, int x0, int y0, int w, int h, uint32_t color) {
    openc_normalize_rect(&x0, &y0, &w, &h);
    assert(w >= 0 && h >= 0);

    int x1 = x0 + w;
    int y1 = y0 + h;

    for (int x = x0; x < x1; ++x) {
        openc_render_point(canvas, x, y0, color);
    }

    for (int y = y0; y < y1; ++y) {
        openc_render_point(canvas, x0, y, color);
    }

    for (int x = x1; x > x0; --x) {
        openc_render_point(canvas, x, y1, color);
    }

    for (int y = y1; y > y0; --y) {
        openc_render_point(canvas, x1, y, color);
    }
}

void openc_draw_circle(openc_canvas_t* canvas, int cx, int cy, int r, uint32_t color) {
    assert(r >= 0);

    int x1 = cx - r;
    int y1 = cy - r;
    int x2 = cx + r;
    int y2 = cy + r;
    for (int y = y1; y <= y2; ++y) {
        for (int x = x1; x <= x2; ++x) {
            int dx = x - cx;
            int dy = y - cy;
            int dis2 = dx*dx + dy*dy;
            // TODO: Better
            if (dis2 >= pow(r-0.7071067811865476, 2) && dis2 <= pow(r+0.7071067811865476, 2)) {
                openc_render_point(canvas, x, y, color);
            }
        }
    }
}

void openc_draw_triangle(openc_canvas_t* canvas, int x1, int y1, int x2, int y2, int x3, int y3, uint32_t color) {
    // TODO: overlap 
    openc_draw_line(canvas, x1, y1, x2, y2, color);
    openc_draw_line(canvas, x2, y2, x3, y3, color);
    openc_draw_line(canvas, x1, y1, x3, y3, color);
}

void openc_fill(openc_canvas_t* canvas, uint32_t color) {
    for (size_t i = 0; i < canvas->height * canvas->width; ++i) {
        canvas->pixels[i] = color;
    }
}

void openc_fill_rect(openc_canvas_t* canvas, int x0, int y0, int w, int h, uint32_t color) {
    openc_normalize_rect(&x0, &y0, &w, &h);

    for (int dy = 0; dy < h; ++dy) {
        int y = y0 + dy;    
        for (int dx = 0; dx < w; ++dx) {
            int x = x0 + dx;
            openc_render_point(canvas, x, y, color);
        }
    }
}

void openc_fill_circle(openc_canvas_t* canvas, int cx, int cy, int r, uint32_t color) {
    assert(r >= 0);

    int x1 = cx - r;
    int y1 = cy - r;
    int x2 = cx + r;
    int y2 = cy + r;
    for (int y = y1; y <= y2; ++y) {
        for (int x = x1; x <= x2; ++x) {
            int dx = x - cx;
            int dy = y - cy;
            if (dx*dx + dy*dy <= r*r) {
                openc_render_point(canvas, x, y, color);
            }
        }
    }
}

void openc_fill_triangle(openc_canvas_t* canvas, int x1, int y1, int x2, int y2, int x3, int y3, uint32_t color) {
    openc_sort_triangle_points_by_y(&x1, &y1, &x2, &y2, &x3, &y3);

    int dx12 = x2 - x1;
    int dy12 = y2 - y1;
    int dx13 = x3 - x1;
    int dy13 = y3 - y1;

    for (int y = y1; y <= y2; ++y) {
        int s1 = dy12 != 0 ? (y - y1) * dx12 / dy12 + x1 : x1;
        int s2 = dy13 != 0 ? (y - y1) * dx13 / dy13 + x1: x1;
        if (s1 > s2) OPENC_SWAP_VAR(int, s1, s2);
        for (int x = s1; x <= s2; ++x) {
            openc_render_point(canvas, x, y, color);
        }
    }

    int dx32 = x2 - x3;
    int dy32 = y2 - y3;
    int dx31 = x1 - x3;
    int dy31 = y1 - y3;

    for (int y = y2+1; y <= y3; ++y) {
        int s1 = dy32 != 0 ? (y - y3) * dx32 / dy32 + x3 : x3;
        int s2 = dy31 != 0 ? (y - y3) * dx31 / dy31 + x3 : x3;
        if (s1 > s2) OPENC_SWAP_VAR(int, s1, s2);
        for (int x = s1; x <= s2; ++x) {
            openc_render_point(canvas, x, y, color);
        }
    }
}

#define RETURN_DEFER(value) do { result = (value); goto defer; } while (0)

typedef int error_t;

error_t openc_save_to_ppm_file(openc_canvas_t* canvas, const char* filepath) {
    int result = 0;
    FILE* file = NULL;

    {
        file = fopen(filepath, "wb");
        if (file == NULL) RETURN_DEFER(errno);

        fprintf(file, "P6\n%zu %zu 255\n", (size_t)canvas->width, (size_t)canvas->height);
        if (ferror(file)) RETURN_DEFER(errno);

        for (int i = 0; i < canvas->width*canvas->height; ++i) {
            uint32_t pixel = canvas->pixels[i];
            uint8_t bytes[3] = {
                RGBA_UNPACK_R(pixel),
                RGBA_UNPACK_G(pixel),
                RGBA_UNPACK_B(pixel),
            };
            fwrite(bytes, sizeof(bytes), 1, file);
            if (ferror(file)) RETURN_DEFER(errno);
        }
    }

defer:
    if (file != NULL) fclose(file);
    return result;
}

static void openc_normalize_rect(int* x0, int* y0, int* w, int* h) {
    if (*w < 0) {
        *w = -*w;
        *x0 -= *w;
    }

    if (*h < 0) {
        *h = -*h;
        *y0 -= *h;
    }
}

static void openc_sort_triangle_points_by_y(int* x1, int* y1,
                                           int* x2, int* y2,
                                           int* x3, int* y3) 
{
    if (*y1 > *y2) {
        OPENC_SWAP_VAR(int, *x1, *x2);
        OPENC_SWAP_VAR(int, *y1, *y2);
    }
 
    if (*y2 > *y3) {
        OPENC_SWAP_VAR(int, *x2, *x3);
        OPENC_SWAP_VAR(int, *y2, *y3);
    }

    if (*y1 > *y2) {
        OPENC_SWAP_VAR(int, *x1, *x2);
        OPENC_SWAP_VAR(int, *y1, *y2);
    }
}